package com.katesoft.scale4j.rttp.spring.postprocessor;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.katesoft.scale4j.common.services.IBeanNameReferences;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.model.RevisionDomainEntity;
import com.katesoft.scale4j.persistent.model.unified.IBO;
import com.katesoft.scale4j.persistent.spring.LocalAnnotationSessionFactory;
import com.katesoft.scale4j.rttp.internal.RttpHazelcastBridgeAwareImpl;
import org.apache.commons.lang.mutable.MutableLong;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.envers.configuration.AuditConfiguration;
import org.hibernate.jdbc.Work;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Map.Entry;

import static com.katesoft.scale4j.persistent.model.unified.BO.PROP_UID;
import static com.katesoft.scale4j.rttp.internal.IRttpHazelcastRefs.ID_GENERATOR_MAP;
import static java.lang.Math.max;
import static org.apache.commons.lang.StringUtils.rightPad;
import static org.hibernate.EntityMode.POJO;
import static org.hibernate.mapping.Table.qualify;
import static org.springframework.jdbc.support.JdbcUtils.closeResultSet;
import static org.springframework.jdbc.support.JdbcUtils.closeStatement;

/**
 * This class is created for loading max identifier value from database for each domain entity class.
 * <p/>
 * After this jvmcluster will load this data into memory and id generation will be just in-memory(distributed id generator used for this).
 *
 * @author kate2007
 */
public class RttpIdInitializerPostProcessor extends RttpHazelcastBridgeAwareImpl implements BeanPostProcessor
{
    private Logger logger = LogFactory.getLogger(getClass());

    @Override
    public Object postProcessBeforeInitialization(Object bean,
                                                  String beanName) throws BeansException
    {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean,
                                                 String beanName) throws BeansException
    {
        if (IBeanNameReferences.SESSION_FACTORY.equalsIgnoreCase(beanName) && bean instanceof LocalAnnotationSessionFactory) {
            logger.debug("post processing %s", bean);
            final LocalAnnotationSessionFactory factory = (LocalAnnotationSessionFactory) bean;
            final Configuration configuration = factory.getConfiguration();
            HazelcastInstance instance = getBridge().getHazelcastInstance();
            if (instance.getLifecycleService().isRunning()) {
                final IMap<Object, Object> map = instance.getMap(ID_GENERATOR_MAP);
                //
                final HibernateTemplate hibernateTemplate = new HibernateTemplate(factory.getObject());
                hibernateTemplate.execute(new HibernateCallback<Object>()
                {
                    @SuppressWarnings({"deprecation"})
                    @Override
                    public Object doInHibernate(Session session) throws HibernateException, SQLException
                    {
                        logger.debug("************** Persistent classes mapping **************");
                        StringBuilder builder = new StringBuilder();
                        Map<String, ClassMetadata> classMetadata = session.getSessionFactory().getAllClassMetadata();
                        for (Entry<String, ClassMetadata> entry : classMetadata.entrySet()) {
                            AbstractEntityPersister meta = (AbstractEntityPersister) entry.getValue();
                            //
                            String entityName = entry.getKey();
                            PersistentClass persistentClass = configuration.getClassMapping(entityName);
                            String tableName = persistentClass.getTable().getName();
                            Class mappedClass = meta.getMappedClass(POJO);
                            boolean isInternal = (IBO.class.isAssignableFrom(mappedClass) || RevisionDomainEntity.class.isAssignableFrom(mappedClass));
                            if (isInternal && !map.containsKey(mappedClass.getName())) {
                                long l = queryMaxIdentifier(persistentClass, session, configuration);
                                logger.debug(String.format("[%s | %s | %s] -> %s", rightPad(tableName, 25), rightPad(entityName, 75),
                                                           rightPad(mappedClass.getName(), 75), l));
                                map.putIfAbsent(mappedClass.getName(), l);
                            }
                        }
                        logger.debug(builder.toString());
                        logger.debug("********************************************************");
                        return null;
                    }
                });
            }
        }
        return bean;
    }

    protected long queryMaxIdentifier(final PersistentClass persistentClass,
                                      final Session session,
                                      Configuration configuration) throws SQLException
    {
        final MutableLong l1 = new MutableLong(0);
        final MutableLong l2 = new MutableLong(0);
        //
        final Column column = (Column) persistentClass.getProperty(PROP_UID).getColumnIterator().next();
        String t1 = qualify(persistentClass.getTable().getCatalog(), persistentClass.getTable().getSchema(), persistentClass.getTable().getName());
        final String sql1 = String.format("select max(tbl.%s) from %s tbl", column.getName(), t1);
        session.doWork(new Work()
        {
            @Override
            public void execute(Connection connection) throws SQLException
            {
                PreparedStatement st1 = connection.prepareCall(sql1);
                ResultSet rs1 = st1.executeQuery();
                try {
                    if (rs1.next()) {
                        l1.setValue(rs1.getLong(1));
                    }
                }
                catch (SQLException e) {
                    logger.error(String.format("unable to execute %s", sql1), e);
                    throw e;
                }
                finally {
                    closeResultSet(rs1);
                    closeStatement(st1);
                }
            }
        });
        AuditConfiguration auditCfg = AuditConfiguration.getFor(configuration);
        if (auditCfg.getEntCfg().isVersioned(persistentClass.getEntityName())) {
            String t2 = qualify(persistentClass.getTable().getCatalog(), persistentClass.getTable().getSchema(),
                                auditCfg.getAuditEntCfg().getAuditTableName(persistentClass.getEntityName(), persistentClass.getTable().getName()));
            final String sql2 = String.format("select max(tbl.%s) from %s tbl", column.getName(), t2);
            session.doWork(new Work()
            {
                @Override
                public void execute(Connection connection) throws SQLException
                {
                    PreparedStatement st2 = connection.prepareCall(sql2);
                    ResultSet rs2 = st2.executeQuery();
                    try {
                        if (rs2.next()) {
                            l2.setValue(rs2.getLong(1));
                        }
                    }
                    catch (SQLException e) {
                        logger.error(String.format("unable to execute %s", sql2), e);
                        throw e;
                    }
                    finally {
                        closeResultSet(rs2);
                        closeStatement(st2);
                    }
                }
            });
        }
        return max(l1.longValue(), l2.longValue());
    }
}